import {
  ApplicationRef,
  Component,
  ComponentFactoryResolver,
  ElementRef,
  EventEmitter,
  Injector,
  Input,
  OnDestroy,
  ViewContainerRef,
  Output,
  HostBinding,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { ComponentPortal, DomPortalOutlet } from '@angular/cdk/portal';
import { Subscription } from 'rxjs';

import { ApiViewerComponent } from './api-viewer/api-viewer.component';
import { ExampleViewerComponent } from './example-viewer/example-viewer.component';
import { ModelViewerComponent } from './model-viewer/model-viewer.component';
import apis from '../../__auto__/api';
import models from '../../__auto__/model';

@Component({
  selector: 'fui-doc-viewer',
  template: '<div class="docs-loading">Loading document...</div>',
  styles: [':host { display: block }'],
})
export class DocViewerComponent implements OnDestroy {
  @HostBinding('class.fui-doc-viewer') hostClass = true;

  private _portalHosts: DomPortalOutlet[] = [];
  private _documentFetchSubscription: Subscription;

  /** The URL of the document to display. */
  @Input()
  set documentUrl(url: string) {
    this._fetchDocument(url);
  }

  @Input() example: string;

  @Output() contentLoaded = new EventEmitter<void>();

  /** The document text. It should not be HTML encoded. */
  textContent = '';

  get hasApi() {
    if (!this.example) {
      return false;
    }

    const api = apis[this.example];
    if (!api) {
      return false;
    }
    if (!api.some((_api) => _api.apis.length > 0)) {
      return false;
    }

    return true;
  }

  get hasModel() {
    if (!this.example) {
      return false;
    }

    const exampleModels = models[this.example];
    if (!exampleModels) {
      return false;
    }

    return exampleModels.length > 0;
  }

  constructor(
    private _appRef: ApplicationRef,
    private _componentFactoryResolver: ComponentFactoryResolver,
    private _elementRef: ElementRef,
    private _http: HttpClient,
    private _injector: Injector,
    private _viewContainerRef: ViewContainerRef,
    private _router: Router,
  ) { }

  /** Fetch a document by URL. */
  private _fetchDocument(url: string) {
    // Cancel previous pending request
    if (this._documentFetchSubscription) {
      this._documentFetchSubscription.unsubscribe();
    }

    this._documentFetchSubscription = this._http.get(url, {responseType: 'text'}).subscribe(
      response => {
        this._elementRef.nativeElement.innerHTML = response;
        this.textContent = this._elementRef.nativeElement.textContent;
        this._loadComponents('fui-docs-example', ExampleViewerComponent);
        this._loadApis();
        this._loadModels();
        setTimeout(() => {
          this._fixFragmentUrls();
          this.contentLoaded.next();
        }, 0);
      },
      error => {
        this._elementRef.nativeElement.innerText =
          `Failed to load document: ${url}. Error: ${JSON.stringify(error)}`;
      });
  }

  releadLiveExamples() {
    // When the example viewer is dynamically loaded inside of md-tabs, they somehow end up in
    // the wrong place in the DOM after switching tabs. This function is a workaround to
    // put the live examples back in the right place.
    this._clearLiveExamples();
    this._loadComponents('fui-docs-example', ExampleViewerComponent);
    this._loadApis();
    this._loadModels();
    setTimeout(() => {
      this._fixFragmentUrls();
    }, 0);
  }

  /** Instantiate a ExampleViewer for each example. */
  private _loadComponents(componentName: string, componentClass: any) {
    const exampleElements =
      this._elementRef.nativeElement.querySelectorAll(`[${componentName}]`);

    Array.prototype.slice.call(exampleElements).forEach((element: Element) => {
      const example = element.getAttribute(componentName);

      const portalHost = new DomPortalOutlet(
        element, this._componentFactoryResolver, this._appRef, this._injector);
      const examplePortal = new ComponentPortal(componentClass, this._viewContainerRef);
      const exampleViewer = portalHost.attach(examplePortal);
      (exampleViewer.instance as ExampleViewerComponent).example = example;

      this._portalHosts.push(portalHost);
    });
  }

  private _loadApis() {
    if (!this.hasApi) {
      return;
    }

    const apiElement =
      this._elementRef.nativeElement.querySelector(`.docs-api`);

    const portalHost = new DomPortalOutlet(
      apiElement, this._componentFactoryResolver, this._appRef, this._injector);
    const apiPortal = new ComponentPortal(ApiViewerComponent, this._viewContainerRef);
    const apiViewer = portalHost.attach(apiPortal);
    (apiViewer.instance as ApiViewerComponent).example = this.example;

    this._portalHosts.push(portalHost);
  }

  private _loadModels() {
    if (!this.hasModel) {
      return;
    }

    const modelElement =
      this._elementRef.nativeElement.querySelector(`.docs-model`);

    const portalHost = new DomPortalOutlet(
      modelElement, this._componentFactoryResolver, this._appRef, this._injector);
    const modelPortal = new ComponentPortal(ModelViewerComponent, this._viewContainerRef);
    const modelViewer = portalHost.attach(modelPortal);
    (modelViewer.instance as ModelViewerComponent).example = this.example;

    this._portalHosts.push(portalHost);
  }

  private _clearLiveExamples() {
    this._portalHosts.forEach(h => h.dispose());
    this._portalHosts = [];
  }

  /**
   * A fragment link is a link that references a specific element on the page that should be
   * scrolled into the viewport on page load or click.
   *
   * By default those links refer to the root page of the documentation and the fragment links
   * won't work properly. Those links need to be updated to be relative to the current base URL.
   */
  private _fixFragmentUrls() {
    const baseUrl = this._router.url.split('#')[0];
    const anchorElements =
      [].slice.call(this._elementRef.nativeElement.querySelectorAll('a')) as HTMLAnchorElement[];

    // Update hash links that are referring to the same page and host. Links that are referring
    // to a different destination shouldn't be updated. For example the Google Fonts URL.
    anchorElements.filter(anchorEl => anchorEl.hash && anchorEl.host === location.host)
      .forEach(anchorEl => anchorEl.href = `${baseUrl}${anchorEl.hash}`);
  }

  ngOnDestroy() {
    this._clearLiveExamples();

    if (this._documentFetchSubscription) {
      this._documentFetchSubscription.unsubscribe();
    }
  }
}
